/*******************************************************************************
 * Copyright (c) 2010 liw.
 * All rights reserved. 
 * 
 * This file is part of VanBus.
 * 
 *     VanBus is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 * 
 *     VanBus is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
 * 
 *     You should have received a copy of the GNU General Public License
 *     along with VanBus.  If not, see <http://www.gnu.org/licenses/>.
 * Contributors:
 *     liw - initial API and implementation
 ******************************************************************************/
package org.niclab.vanbus.gps;

import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import org.niclab.vanbus.application.VanBusApplication;

import android.app.Application;
import android.content.Context;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.util.Log;

public class MyLocationManager {
	
	private static String LOG_TAG="MyLocationManager";
	
	private Application appContext;
	private Timer timer;
	private LocationManager locationManager;
	private LocationListener coarseLocationListener;
	private LocationListener fineLocationListener;
	
	private static final long coarseLocUpdateInterval = 500; //ms
	private static final long fineLocUpdateInterval = 500; //ms
	
	private static final long coarseLocUpdateDistance = 1000; //meters
	private static final long fineLocUpdateDistance = 50; //meters
	private static final int TWO_MINUTES = 1000 * 60 * 2;
	private static final int HALF_A_MINUTE = 1000*30;
	private Location currentBestLocation;
//	boolean gps_enabled=false;
//	boolean network_enabled=false;

	
	
	public MyLocationManager(Application app){
		appContext = app;
		locationManager = (LocationManager) appContext.getSystemService(Context.LOCATION_SERVICE);
		currentBestLocation = getLastKnownLocation();
	}
	
	public boolean isGPSEnabled(){
		return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
	}
	
	public boolean isNetworkEnabled(){
		return locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER);
		
	}
	public boolean isGPSandNetworkEnabled(){
		return isGPSEnabled() && isNetworkEnabled();
		
	}
	
	public boolean isOnlyGPSEnabled(){
		return isGPSEnabled() && !isNetworkEnabled();
	}
	
	public boolean isOnlyNetworkEnabled(){
		return isNetworkEnabled() && !isGPSEnabled();
	}
	
	public boolean isGPSorNetworkEnabled(){
		return isGPSEnabled() || isNetworkEnabled();
	}
	
	public void startPinDownMyLocation(){
		
		//clear all the old listeners
		if(coarseLocationListener !=null)
			locationManager.removeUpdates(coarseLocationListener);
		if(fineLocationListener !=null)
			locationManager.removeUpdates(fineLocationListener);
		
		// Initialize criteria for location providers
		Criteria fine = new Criteria();
		fine.setAccuracy(Criteria.ACCURACY_FINE);
			 
		Criteria coarse = new Criteria();
		coarse.setAccuracy(Criteria.ACCURACY_COARSE);
			 
			 
		creatLocationListeners();
		// Will keep updating about every 500 ms until accuracy is about 1000 meters to get quick fix.
		locationManager.requestLocationUpdates(locationManager.getBestProvider(coarse, true),0, 0, coarseLocationListener);
		// Will keep updating about every 500 ms until accuracy is about 50 meters to get accurate fix.
		locationManager.requestLocationUpdates(locationManager.getBestProvider(fine, true),0, 0, fineLocationListener);
		
	}
	
	private void creatLocationListeners(){
		coarseLocationListener = new LocationListener(){
	
			@Override
			public void onLocationChanged(Location location) {
				
				 Log.v(LOG_TAG,"Location updated in coarseLocListener");
				 if(isBetterLocation(location, currentBestLocation))
					 currentBestLocation = location;
				 
				 if (location.getAccuracy() < coarseLocUpdateDistance && location.hasAccuracy()){
					 locationManager.removeUpdates(this);
					 coarseLocationListener = null;
				 }
		
						
			}
	
			@Override
			public void onProviderDisabled(String provider) {
						
			}
	
			@Override
			public void onProviderEnabled(String provider) {
					
			}
	
			@Override
			public void onStatusChanged(String provider, int status,
					Bundle extras) {
						
			}
			
		};
		
	
		fineLocationListener = new LocationListener(){
	
			@Override
			public void onLocationChanged(Location location) {
				 Log.v(LOG_TAG,"Location updated in fineLocListener");
				 if(isBetterLocation(location, currentBestLocation))
					 currentBestLocation = location;
		        if (location.getAccuracy() <fineLocUpdateDistance   && location.hasAccuracy()){
		             locationManager.removeUpdates(this);
		             fineLocationListener = null;
		           }
		
		
				
			}
	
			@Override
			public void onProviderDisabled(String provider) {
				
			}
	
			@Override
			public void onProviderEnabled(String provider) {
			
			}
	
			@Override
			public void onStatusChanged(String provider, int status,
					Bundle extras) {
							
			}
			
		};
		
	}
	
	public Location getLastKnownLocation(){
		if(isGPSEnabled())
			return locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
		else if(isNetworkEnabled())
			return locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
		else return null;
		
		
	}
	
	/**
	 * get current location. If ms >0, then this starts a new search location process and 
	 * blocks the current thread for that long, then return a new location.
	 * If ms ==0, then return the current best location immediately.
	 *
	 * @param ms how long it waits to get the current location;
	 * @return the current best location
	 */
	
	public Location getCurrentBestLocation(long ms){
		if(ms==0)
			return currentBestLocation;
		
		
		//if current best location is updated within 30seconds, we return it immediately.
		if(currentBestLocation!=null){
			if(System.currentTimeMillis()-currentBestLocation.getTime()<HALF_A_MINUTE)
				return currentBestLocation;
		}
		//if the current listeners haven't been all removed, we do not start new listeners.
		if(coarseLocationListener == null && fineLocationListener ==null)
			this.startPinDownMyLocation();
		
		VanBusApplication app = (VanBusApplication) appContext;
		
		ScheduledFuture<Location> futureLoc= app.getScheduledExecutorService().schedule(new Callable<Location>(){

			@Override
			public Location call() throws Exception {
				
				return currentBestLocation;
			}
			
			
		}, ms, TimeUnit.MILLISECONDS);
		
		
		try {
			return futureLoc.get();
		} catch (Exception e) {
			e.printStackTrace();
			return currentBestLocation;
			
		}
		
	}
	
	
	
	
	/** Determines whether one Location reading is better than the current Location fix
	  * @param location  The new Location that you want to evaluate
	  * @param currentBestLocation  The current Location fix, to which you want to compare the new one
	  */

	private boolean isBetterLocation(Location location, Location currentBestLocation) {
	    if (currentBestLocation == null) {
	        // A new location is always better than no location
	        return true;
	    }

	    // Check whether the new location fix is newer or older
	    long timeDelta = location.getTime() - currentBestLocation.getTime();
	    boolean isSignificantlyNewer = timeDelta > TWO_MINUTES;
	    boolean isSignificantlyOlder = timeDelta < -TWO_MINUTES;
	    boolean isNewer = timeDelta > 0;

	    // If it's been more than two minutes since the current location, use the new location
	    // because the user has likely moved
	    if (isSignificantlyNewer) {
	        return true;
	    // If the new location is more than two minutes older, it must be worse
	    } else if (isSignificantlyOlder) {
	        return false;
	    }

	    // Check whether the new location fix is more or less accurate
	    int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy());
	    boolean isLessAccurate = accuracyDelta > 0;
	    boolean isMoreAccurate = accuracyDelta < 0;
	    boolean isSignificantlyLessAccurate = accuracyDelta > 200;

	    // Check if the old and new location are from the same provider
	    boolean isFromSameProvider = isSameProvider(location.getProvider(),
	            currentBestLocation.getProvider());

	    // Determine location quality using a combination of timeliness and accuracy
	    if (isMoreAccurate) {
	        return true;
	    } else if (isNewer && !isLessAccurate) {
	        return true;
	    } else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) {
	        return true;
	    }
	    return false;
	}

	/** Checks whether two providers are the same */
	private boolean isSameProvider(String provider1, String provider2) {
	    if (provider1 == null) {
	      return provider2 == null;
	    }
	    return provider1.equals(provider2);
	}
	


	

}

/*public class MyLocation {
    Timer timer1;
    LocationManager lm;
    LocationResult locationResult;
    boolean gps_enabled=false;
    boolean network_enabled=false;

    public boolean getLocation(Context context, LocationResult result)
    {
        //I use LocationResult callback class to pass location value from MyLocation to user code.
        locationResult=result;
        if(lm==null)
            lm = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);

        //exceptions will be thrown if provider is not permitted.
        try{gps_enabled=lm.isProviderEnabled(LocationManager.GPS_PROVIDER);}catch(Exception ex){}
        try{network_enabled=lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER);}catch(Exception ex){}

        //don't start listeners if no provider is enabled
        if(!gps_enabled && !network_enabled)
            return false;

        if(gps_enabled)
            lm.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, locationListenerGps);
        if(network_enabled)
            lm.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, locationListenerNetwork);
        timer1=new Timer();
        timer1.schedule(new GetLastLocation(), 20000);
        return true;
    }

    LocationListener locationListenerGps = new LocationListener() {
        public void onLocationChanged(Location location) {
            timer1.cancel();
            locationResult.gotLocation(location);
            lm.removeUpdates(this);
            lm.removeUpdates(locationListenerNetwork);
        }
        public void onProviderDisabled(String provider) {}
        public void onProviderEnabled(String provider) {}
        public void onStatusChanged(String provider, int status, Bundle extras) {}
    };

    LocationListener locationListenerNetwork = new LocationListener() {
        public void onLocationChanged(Location location) {
            timer1.cancel();
            locationResult.gotLocation(location);
            lm.removeUpdates(this);
            lm.removeUpdates(locationListenerGps);
        }
        public void onProviderDisabled(String provider) {}
        public void onProviderEnabled(String provider) {}
        public void onStatusChanged(String provider, int status, Bundle extras) {}
    };

    class GetLastLocation extends TimerTask {
        @Override
        public void run() {
             lm.removeUpdates(locationListenerGps);
             lm.removeUpdates(locationListenerNetwork);

             Location net_loc=null, gps_loc=null;
             if(gps_enabled)
                 gps_loc=lm.getLastKnownLocation(LocationManager.GPS_PROVIDER);
             if(network_enabled)
                 net_loc=lm.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);

             //if there are both values use the latest one
             if(gps_loc!=null && net_loc!=null){
                 if(gps_loc.getTime()>net_loc.getTime())
                     locationResult.gotLocation(gps_loc);
                 else
                     locationResult.gotLocation(net_loc);
                 return;
             }

             if(gps_loc!=null){
                 locationResult.gotLocation(gps_loc);
                 return;
             }
             if(net_loc!=null){
                 locationResult.gotLocation(net_loc);
                 return;
             }
             locationResult.gotLocation(null);
        }
    }

    public static abstract class LocationResult{
        public abstract void gotLocation(Location location);
    }
}*/
